/**
 * Tencent is pleased to support the open source community by making QMUI_iOS available.
 * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
 * http://opensource.org/licenses/MIT
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */

//
//  QMUIPopupContainerView.h
//  qmui
//
//  Created by QMUI Team on 15/12/17.
//

#import <UIKit/UIKit.h>
#import "UIControl+QMUI.h"

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
    QMUIPopupContainerViewLayoutDirectionAbove,
    QMUIPopupContainerViewLayoutDirectionBelow,
    QMUIPopupContainerViewLayoutDirectionLeft,
    QMUIPopupContainerViewLayoutDirectionRight
};

typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutAlignment) {
    QMUIPopupContainerViewLayoutAlignmentCenter,
    QMUIPopupContainerViewLayoutAlignmentLeading,
    QMUIPopupContainerViewLayoutAlignmentTrailing,
};

/**
 * 带箭头的小tips浮层，自带 imageView 和 textLabel，可展示简单的图文信息，支持 UIViewContentModeTop/UIViewContentModeBottom/UIViewContentModeCenter 三种布局方式。
 * QMUIPopupContainerView 支持以两种方式显示在界面上：
 * 1. 添加到某个 UIView 上（适合于 viewController 切换时浮层跟着一起切换的场景），这种场景只能手动隐藏浮层。
 * 2. 在 QMUIPopupContainerView 自带的 UIWindow 里显示（适合于用完就消失的场景，不要涉及界面切换），这种场景支持点击空白地方自动隐藏浮层。
 *
 * 使用步骤：
 * 1. 调用 init 方法初始化。
 * 2. 选择一种显示方式：
 * 2.1 如果要添加到某个 UIView 上，则先设置浮层 hidden = YES，然后调用 addSubview: 把浮层添加到目标 UIView 上。
 * 2.2 如果是轻量的场景用完即走，则 init 完浮层即可，无需设置 hidden，也无需调用 addSubview:，在后面第 4 步里会自动把浮层添加到 UIWindow 上显示出来。
 * 3. 通过为 sourceBarItem/sourceView/sourceRect 三者中的一个赋值，来决定浮层布局的位置。
 * 4. 调用 showWithAnimated: 或 showWithAnimated:completion: 显示浮层。
 * 5. 调用 hideWithAnimated: 或 hideWithAnimated:completion: 隐藏浮层。
 *
 * @warning 如果使用方法 2.2，并且没有打开 automaticallyHidesWhenUserTap 属性，则记得在适当的时机（例如 viewWillDisappear:）隐藏浮层。
 *
 * 如果默认功能无法满足需求，可继承它重写一个子类，继承要点：
 * 1. 初始化时要做的事情请放在 didInitialize 里。
 * 2. 所有 subviews 请加到 contentView 上。
 * 3. 通过重写 sizeThatFitsInContentView:，在里面返回当前 subviews 的大小。
 * 4. 在 layoutSubviews: 里，所有 subviews 请相对于 contentView 布局。
 */
@interface QMUIPopupContainerView : UIControl {
    CAShapeLayer    *_backgroundLayer;
    CAShapeLayer    *_borderLayer;// CAShapeLayer 的特性是有一半 stroke 会和 fill 重叠，而我们希望的是 stroke 在 fill 外面，所以只能分开两个 layer 实现 border 和 background
    UIImageView     *_arrowImageView;
    CGFloat         _arrowMinX;
    CGFloat         _arrowMinY;
    BOOL            _shouldInvalidateLayout;
}

@property(nonatomic, assign) BOOL debug;

/// 在浮层显示时，点击空白地方是否要自动隐藏浮层，仅在用方法 2 显示时有效。
/// 默认为 NO，也即需要手动调用代码去隐藏浮层。
@property(nonatomic, assign) BOOL automaticallyHidesWhenUserTap;

/// 所有 subview 都应该添加到 contentView 上，subviews 占据的大小通过 sizeThatFitsInContentView: 或者 contentViewSizeThatFitsBlock 来返回，subviews 的布局通过重写 layoutSubviews 或 qmui_layoutSubviewsBlock 来布局，注意布局时应该基于 contentView。
@property(nonatomic, strong, readonly) UIView *contentView;

/**
 与 sizeThatFitsInContentView: 等价，用于告诉组件，添加到 contentView 上的 subviews 的大小

 @param size 浮层里除去 safetyMarginsOfSuperview、arrowSize、contentEdgeInsets 之外后，留给内容的实际大小，计算 subview 大小时均应使用这个参数来计算
 @return 自定义内容实际占据的大小
 @note 计算结果不需要操心 maximumWidth、minimumWidth，这些会由组件统一处理，你只需要在这个 block 里返回内容的实际大小即可。
 */
@property(nonatomic, copy, nullable) CGSize (^contentViewSizeThatFitsBlock)(CGSize size);

/// 预提供的UIImageView，默认为nil，调用到的时候才初始化
@property(nonatomic, strong, readonly, nullable) UIImageView *imageView;

/// 预提供的UILabel，默认为nil，调用到的时候才初始化。默认支持多行。
@property(nonatomic, strong, readonly, nullable) UILabel *textLabel;

/// 圆角矩形气泡内的padding（不包括三角箭头），默认是(8, 8, 8, 8)
@property(nonatomic, assign) UIEdgeInsets contentEdgeInsets UI_APPEARANCE_SELECTOR;

/// 调整imageView的位置，默认为UIEdgeInsetsZero。top/left正值表示往下/右方偏移，bottom/right仅在对应位置存在下一个子View时生效（例如只有同时存在imageView和textLabel时，imageEdgeInsets.right才会生效）。
@property(nonatomic, assign) UIEdgeInsets imageEdgeInsets UI_APPEARANCE_SELECTOR;

/// 调整textLabel的位置，默认为UIEdgeInsetsZero。top/left/bottom/right的作用同<i>imageEdgeInsets</i>
@property(nonatomic, assign) UIEdgeInsets textEdgeInsets UI_APPEARANCE_SELECTOR;

/// 三角箭头的大小，默认为 CGSizeMake(18, 9)
@property(nonatomic, assign) CGSize arrowSize UI_APPEARANCE_SELECTOR;

/// 三角箭头的图片，通常用于默认的三角样式不满足需求时。当使用了 arrowImage 后，arrowSize 将会被固定为 arrowImage.size。
/// 当 borderWidth 大于0时，arrowImage 会与所在那一侧的 border 重叠，所以你的切图需要预留一部分 borderWidth 的区域以盖住边框。
/// 图片必须为箭头向下的方向
@property(nonatomic, strong, nullable) UIImage *arrowImage UI_APPEARANCE_SELECTOR;

/// 最大宽度（指整个控件的宽度，而不是contentView部分），默认为CGFLOAT_MAX
@property(nonatomic, assign) CGFloat maximumWidth UI_APPEARANCE_SELECTOR;

/// 最小宽度（指整个控件的宽度，而不是contentView部分），默认为0
@property(nonatomic, assign) CGFloat minimumWidth UI_APPEARANCE_SELECTOR;

/// 最大高度（指整个控件的高度，而不是contentView部分），默认为CGFLOAT_MAX，会在布局时被动态修改。
@property(nonatomic, assign) CGFloat maximumHeight UI_APPEARANCE_SELECTOR;

/// 最小高度（指整个控件的高度，而不是contentView部分），默认为0
@property(nonatomic, assign) CGFloat minimumHeight UI_APPEARANCE_SELECTOR;

/// 计算布局时期望的默认位置，默认为QMUIPopupContainerViewLayoutDirectionAbove，也即在目标的上方
@property(nonatomic, assign) QMUIPopupContainerViewLayoutDirection preferLayoutDirection UI_APPEARANCE_SELECTOR;

/// 最终的布局方向（preferLayoutDirection只是期望的方向，但有可能那个方向已经没有剩余空间可摆放控件了，所以会自动变换）
@property(nonatomic, assign, readonly) QMUIPopupContainerViewLayoutDirection currentLayoutDirection;

/// 计算布局时期望浮层与目标位置的对齐方式，默认为 QMUIPopupContainerViewLayoutAlignmentCenter，也即浮层和目标位置相对居中。
/// 对 preferLayoutDirection 为 Above/Below 而言，Leading 表示浮层的左侧与目标位置左边缘对齐，Trailing 表示浮层的右侧与目标位置右边缘对齐。
/// 对 preferLayoutDirection 为 Left/Right 而言，Leading 表示浮层的顶端与目标位置顶边缘对齐，Trailing 表示浮层的底端与目标位置底边缘对齐。
/// 如果预期的对齐方式无法被满足时，会根据 usesOppositeLayoutAlignmentIfNeeded 的值来决定备选方案。
@property(nonatomic, assign) QMUIPopupContainerViewLayoutAlignment preferLayoutAlignment UI_APPEARANCE_SELECTOR;

/// 表示 preferLayoutAlignment 在极端情况下无法满足调用方设置的值时，应该以什么方式作为备选。
/// 若当前属性值为 YES，则表示用相反的对齐方式去尝试（例如 preferLayoutAlignment = QMUIPopupContainerViewLayoutAlignmentLeading 则在极端情况下会用 QMUIPopupContainerViewLayoutAlignmentTrailing 作为备选），若当前属性值为 NO 则表示保持对齐方向不变，让浮层的边缘紧贴着 safetyMarginsOfSuperview 即可。
/// 默认为 YES。
/// @warning 对 QMUIPopupContainerViewLayoutAlignmentCenter 无意义，因为 QMUIPopupContainerViewLayoutAlignmentCenter 没有所谓的相反概念。
@property(nonatomic, assign) BOOL usesOppositeLayoutAlignmentIfNeeded UI_APPEARANCE_SELECTOR;

/// 最终布局时箭头距离目标边缘的距离，默认为5
@property(nonatomic, assign) CGFloat distanceBetweenSource UI_APPEARANCE_SELECTOR;

/// 最终布局时与父节点的边缘的临界点，默认为(10, 10, 10, 10)，注意这里的值不需要由业务考虑 safeAreaInsets，内部会自己叠加。
@property(nonatomic, assign) UIEdgeInsets safetyMarginsOfSuperview UI_APPEARANCE_SELECTOR;

/// 允许用一个自定的 view 作为背景，会自动将其 mask 为圆角带箭头的造型，当同时使用 backgroundView 和 arrowImage 时，arrowImage 只作为遮罩使用（也即使用它的造型，不显示它的图片内容）。
/// 默认为 nil。
@property(nonatomic, strong, nullable) UIView *backgroundView;

/// 浮层的背景色，作用区域为箭头+圆角矩形区域，当同时使用 backgroundView 和 backgroundColor 时，backgroundView 会盖在 backgroundColor 上方。
@property(nonatomic, strong, nullable) UIColor *backgroundColor UI_APPEARANCE_SELECTOR;

/// 浮层点击 highlighted 时的背景色，作用区域为箭头+圆角矩形区域
@property(nonatomic, strong, nullable) UIColor *highlightedBackgroundColor UI_APPEARANCE_SELECTOR;

/// 当使用方法 2 显示并且打开了 automaticallyHidesWhenUserTap 时，可修改背景遮罩的颜色，默认为 UIColorMask，若非使用方法 2，或者没有打开 automaticallyHidesWhenUserTap，则背景遮罩为透明（可视为不存在背景遮罩）
@property(nonatomic, strong, nullable) UIColor *maskViewBackgroundColor UI_APPEARANCE_SELECTOR;

/// 浮层的阴影，默认包含箭头的形状，如果使用了 @c arrowImage 则不包含箭头。当不需要阴影时可将其置为 nil。
@property(nonatomic, strong, nullable) NSShadow *shadow UI_APPEARANCE_SELECTOR;

@property(nonatomic, strong, nullable) UIColor *borderColor UI_APPEARANCE_SELECTOR;
@property(nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR;
@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR;

/// 可以是 UINavigationBar、UIToolbar 上的 UIBarButtonItem，或者 UITabBar 上的 UITabBarItem
@property(nonatomic, weak, nullable) __kindof UIBarItem *sourceBarItem;

@property(nonatomic, weak, nullable) __kindof UIView *sourceView;

/// rect 需要处于 QMUIPopupContainerView 所在的坐标系内，例如如果 popup 使用 addSubview: 的方式添加到界面，则 sourceRect 应该是 superview 坐标系内的；如果 popup 使用 window 的方式展示，则 sourceRect 需要转换为 window 坐标系内。
@property(nonatomic, assign) CGRect sourceRect;

/// 标记为需要更新布局，会在下一次 runloop 里统一调用 updateLayout。一般情况请用这个方法，避免直接用 updateLayout，从而获取更佳的性能。
- (void)setNeedsUpdateLayout;

/// 立即刷新当前 popup 的布局，前提是 popup.isShowing 为 YES。
- (void)updateLayout;

- (void)showWithAnimated:(BOOL)animated;
- (void)showWithAnimated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion;
- (void)hideWithAnimated:(BOOL)animated;
- (void)hideWithAnimated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion;
- (BOOL)isShowing;

/// 允许业务自定义显示动画，请在这个 block 里写自己的动画实现，并在恰当的时候调用参数里的 animation() 和 completion()。
/// @param defaultAnimation 组件里默认的显示动画（默认实现是 scale+alpha），业务可自行决定是否要调用它
/// @param completion 动画结束的回调，业务必须在自己动画结束时主动调用它
/// @param isWindowMode 是否正在以 window 模式展示
/// @param rootView popup 组件当前所在的容器，如果是 window 模式，则是内部自己创建的 window.rootViewController.view，若是其他模式则是业务的 view
/// @param popup 当前 popup 实例
@property(nonatomic, copy) void (^showingAnimationBlock)(void (^defaultAnimation)(void), void (^completion)(BOOL finished), BOOL isWindowMode, UIView * _Nullable rootView, __kindof QMUIPopupContainerView *popup);

/// 允许业务自定义隐藏动画，请在这个 block 里写自己的动画实现，并在恰当的时候调用参数里的 animation() 和 completion()。
/// @param defaultAnimation 组件里默认的显示动画（默认实现是 scale+alpha），业务可自行决定是否要调用它
/// @param completion 动画结束的回调，业务必须在自己动画结束时主动调用它
/// @param isWindowMode 是否正在以 window 模式展示
/// @param rootView popup 组件当前所在的容器，如果是 window 模式，则是内部自己创建的 window.rootViewController.view，若是其他模式则是业务的 view
/// @param popup 当前 popup 实例
@property(nonatomic, copy) void (^hidingAnimationBlock)(void (^defaultAnimation)(void), void (^completion)(BOOL finished), BOOL isWindowMode, UIView * _Nullable rootView, __kindof QMUIPopupContainerView *popup);

/**
 *  即将显示时的回调
 *  注：如果需要使用例如 didShowBlock 的时机，请使用 @showWithAnimated:completion: 的 completion 参数来实现。
 *  @argv animated 是否需要动画
 */
@property(nonatomic, copy, nullable) void (^willShowBlock)(BOOL animated);

/**
 *  即将隐藏时的回调
 *  @argv hidesByUserTap 用于区分此次隐藏是否因为用户手动点击空白区域导致浮层被隐藏
 *  @argv animated 是否需要动画
 */
@property(nonatomic, copy, nullable) void (^willHideBlock)(BOOL hidesByUserTap, BOOL animated);

/**
 *  已经隐藏后的回调
 *  @argv hidesByUserTap 用于区分此次隐藏是否因为用户手动点击空白区域导致浮层被隐藏
 */
@property(nonatomic, copy, nullable) void (^didHideBlock)(BOOL hidesByUserTap);

@end

@interface QMUIPopupContainerView (UISubclassingHooks)

/// 子类重写，在初始化时做一些操作
- (void)didInitialize NS_REQUIRES_SUPER;

/**
 子类重写，用于告诉父类添加到 contentView 上的 subviews 的大小

 @param size 浮层里除去 safetyMarginsOfSuperview、arrowSize、contentEdgeInsets 之外后，留给内容的实际大小，计算 subview 大小时均应使用这个参数来计算
 @return 自定义内容实际占据的大小
 @note 计算结果不需要操心 maximumWidth、minimumWidth，这些会由组件统一处理，你只需要在这个方法里返回内容的实际大小即可。
 */
- (CGSize)sizeThatFitsInContentView:(CGSize)size;
@end

NS_ASSUME_NONNULL_END
